﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-09-21 19:35
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using BassDrop.Core.Common;
using BassDrop.Core.Common.Cloud;
using BassDrop.Core.Common.Exceptions;
using BassDrop.Core.Common.Media;
using BassDrop.Core.Common.Plugins;
using BassDrop.Core.Common.Services;

namespace BassDrop.Core.Desktop.Plugins
{
	/// <summary>
	/// Stores searchable references to loaded plugins
	/// </summary>
	public class PluginManager : PluginManagerBase
	{
		/// <summary>
		/// Searches the passed path for dlls, checks if they are valid plugins and if
		/// they are, loads them
		/// </summary>
		/// <param name="path">path to a folder containing dlls</param>
		/// <param name="recursive">if true or left out, the method will check the entire tree for plugins</param>
		public async Task LoadPluginsFrom( string path, bool recursive = true )
		{
			// Add resolver to the current domain
			AppDomain.CurrentDomain.AssemblyResolve -= CurrentDomain_AssemblyResolve;
			AppDomain.CurrentDomain.AssemblyResolve += CurrentDomain_AssemblyResolve;

			if ( Directory.Exists( path ) )
			{
				CompoundException exception = new CompoundException();

				foreach ( var item in Directory.GetFiles( path ) )
				{
					if ( Path.GetExtension( item ).ToLower() == ".dll" && IsPlugin( item ) )
					{
						try
						{
							// Is the dll a valid plugin
							bool isValid = false;
							// Descriptor object for the plugin
							BassDropPluginBase descriptor = null;

							try
							{
								// Load dll
								Assembly plugin = Assembly.LoadFrom( item );

								// Check if it is a valid plugin and create objects
								Type descriptorType = GetTypeForSimpleName( plugin, "BassDropPlugin" );

								if ( descriptorType != null )
								{
									descriptor = Activator
										.CreateInstance( descriptorType, Controller.Instance, Path.GetDirectoryName( plugin.Location ) )
										as BassDropPluginBase;

									if ( descriptor != null )
									{
										await descriptor.Load( Controller.Instance.ApplicationSettings );
										isValid = true;
									}
									else
									{
										System.Diagnostics.Debug.Assert( false, string.Format(
													"The plugin descriptor for {0} is invalid:\r\n{1}",
													item,
													(descriptor == null ? "null" : descriptor.ToString()) ) );
									}
								}
							}
							catch ( Exception e )
							{
								System.Diagnostics.Debug.Assert( false, "An error occured when loading the plugins:\r\n" + e.ToString() );
							}

							if ( isValid )
							{
								// Add plugin
								AddPlugin( descriptor, descriptor.SupportedProtocols.ToArray() );
							}
							else
							{
								// WARNING: this trashes the current AppDomain
								System.Diagnostics.Debug.Assert( false, string.Format( "The plugin {0} is invalid.\r\n\r\nWARNING: the current AppDomain is trashed now.", item ) );
							}
						}
						catch ( Exception e )
						{
							System.Diagnostics.Debug.Assert( false, "An error occured when loading the plugins:\r\n" + e.ToString() );
						}
					}
				}

				if ( recursive )
				{
					foreach ( var item in Directory.GetDirectories( path ) )
					{
						try
						{
							await LoadPluginsFrom( item );
						}
						catch ( Exception e )
						{
							exception.Add( e );
						}
					}
				}

				exception.ThrowIfNotEmpty();
			}
		}

		#region Helper methods

		/// <summary>
		/// Returns a type from the passed assembly with the passed unqualified name name.
		/// If multiple types with the name exist, the first one is returned, so only use this
		/// if you are sure that only one class matches this name.
		/// </summary>
		/// <param name="assembly">The assembly to look for the type in</param>
		/// <param name="typeName">The simple, unqualified name of the type to look for</param>
		/// <returns>A Type or null if none found</returns>
		private static Type GetTypeForSimpleName( Assembly assembly, string typeName )
		{
			return (from type
					in assembly.GetTypes()
					where type.Name == typeName
					select type)
					.FirstOrDefault();
		}

		/// <summary>
		/// Returns if the dll with the given absolute path looks like a valid plugin assembly
		/// </summary>
		/// <param name="path">Absolute path to a dll file</param>
		/// <returns></returns>
		private bool IsPlugin( string path )
		{
			string pointerFileName = path.Substring( 0, path.LastIndexOf( '.' ) ) + ".bassdropplugin";

			return ServiceCenter.FileHandler.Exists( pointerFileName );
		}

		/// <summary>
		/// Tries to find assemblies that are not on the search path
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="args"></param>
		/// <returns></returns>
		private Assembly CurrentDomain_AssemblyResolve( object sender, ResolveEventArgs args )
		{
			return FindAssembly( AppDomain.CurrentDomain.BaseDirectory, args.Name );
		}

		/// <summary>
		/// Recursively walks the directory tree under rootDir and loads the first
		/// assembly matching assemblyName it finds. If it finds none it returns null.
		/// </summary>
		/// <param name="rootDir">The path of the root directory to start the search from</param>
		/// <param name="assemblyName">The name of the assembly being looked for</param>
		/// <returns></returns>
		/// <remarks> From: http://stackoverflow.com/a/16700331/301134 </remarks>
		private Assembly FindAssembly( string rootDir, string assemblyName )
		{
			var di = new DirectoryInfo( rootDir );
			var module = di.GetFiles().FirstOrDefault( i => i.Name.ToLower() == assemblyName.ToLower() + ".dll" );
			if ( module != null )
			{
				return Assembly.LoadFrom( module.FullName ); // WARNING: this trashes the current AppDomain
			}

			foreach ( var moduleDir in Directory.GetDirectories( rootDir ) )
			{
				Assembly asm = FindAssembly( moduleDir, assemblyName );

				if ( asm != null )
				{
					return asm;
				}
			}

			return null;
		}

		#endregion Helper methods
	}
}
